iT邦幫忙

2022 iThome 鐵人賽

DAY 10
0

今天起介紹封裝的另一個主題:attributes vs properties(或單數attribute vs property)。


  • 昨天花了很大力氣,設法釐清私有屬性無法用物件.屬性的方式存取(註1),必須利用由類別提供的公開方法(getters和setters)。
  • 但是,這又引發另一個問題。稱問題也許太過沉重,就說是個小小的不便吧。
  • 有甚麼不便之處?各位試想:一般變數可以直接用名稱取值如print(f'{tree_keeper=}')、直接以=賦值如tree_keeper = 'Audrey Hepburn'。而自訂類別中的(私有)屬性,卻必須透過getters和setters方法取值賦值,存取方式明顯不如一般變數的簡單直觀
    class Tree():
        def __init__(self, breed: str, age: int, height: int):
            self.__breed = breed        # private attribute
            self.__age = age            # private attribute
            self.__height = height      # private attribute
    
        def get_age(self) -> int:       # public getter for age
            return self.__age
    
        def set_age(self, age: int):   # public setter for age
            age_ranges = {'camphor': [0, 800], 'oak': [0, 300]}
            if age < age_ranges[self.__breed][0] or age > age_ranges[self.__breed][1]:
                raise Exception('樹齡數字不合理。')
            else:  # 放行
                self.__age = age    
    
    
    # 主程式
    tree_keeper = 'Ingrid Bergman'
    print(f'{tree_keeper=}')         # 一般變數可以直接取值。
    
    tree_keeper = 'Audrey Hepburn'   # 一般變數可以直接賦值。
    print(f'{tree_keeper=}')
    
    tree = Tree('camphor', 50, 37)
    print(f'{tree.get_age()=}')      # 物件的私有屬性要動用getter取值。
    
    tree.set_age(700)                # 物件的私有屬性要動用setter賦值。
    print(f'{tree.get_age()=}')
    
    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220925/20148485Q6hujMD34g.png
  • 有沒有可能讓自訂類別中私有屬性的存取,一方面受到嚴密保護,另一方面使用時享有和一般變數相同的存取方式?如果可能,不是更加方便嗎?
  • 需求為創新之母,可以滿足這個需求的property,這時該出場亮相了。
  • 聽起來,property是個多麼神奇的機制呀。您說神奇當然沒錯,不過骨子裡它只是程式語言的一顆「語法糖」(註2)耳。
  • Property這顆語法糖不是Python獨有,其他一些物件導向的程式語言也有供應。它的具體做法,是將getters和setters方法「偽裝」成屬性。可謂「掛屬性之頭,賣方法之肉」。
  • 筆者到現在還沒有介紹property的中文譯名,是有意為之。太早說明怕讀者反而被譯名搞胡塗,原因是property中文通常也譯作「屬性」。沒錯,就和attribute的中譯同名。所以各位千萬得注意,中文的「屬性」一詞其實是歧義(ambiguous)的,時而指涉attribute,時而卻是property。
  • 最好的辨識方法,是直接看英文。如無英文可看,則從上下文推敲。
  • 為免混淆,本系列文章property一律直接用英文不中譯,中文的「屬性」則專指attribute。

C#的property

  • 據筆者記憶,C#是大量使用property的語言。以下是筆者知道的C# property寫法。不過C#版本改進很快,說不定最新版的語法已經不是這樣。無所謂,本系列重點不在C#。
    using System;
    namespace MyApplication
    {
      class Tree
      {
        private string breed;  // 這是private attribute。
        public string Breed    // 這是public property。
        {
          // 將getter和setter包在property裡面。
          get { return breed; }   // getter
          // 透過public property修改private attribute。
          set { breed = value; }  // setter
        }
      }
    
      class Program
      {
        static void Main(string[] args)
        {
          Tree tree = new Tree();
          tree.Breed = "cedar";   // 直接賦值,表面上沒有使用getter。
          Console.WriteLine(tree.Breed);   // 直接取值,看不到setter影子。
        }
      }
    }
    
    輸出:
    https://ithelp.ithome.com.tw/upload/images/20220925/20148485Y6vBLN1dIx.png

Python的property

  • Python的property機制,是利用內建的property()函數達成。
  • 和print(), type()等內建函數一樣,使用property()不必import任何模組,直接用就行。
  • 範例:
    class Tree:
        def __init__(self, breed: str, age: int):  # constructor建構子
            self.__breed = breed   # __breed和__age是private attribute。
            self.__age = age   # 有些人會錯用「一條」前綴底線來代表private。
    
        def __get_breed(self) -> str:     # private getter
            return self.__breed    
    
        def __get_age(self) -> int:       # private getter
            return self.__age
    
        def __set_age(self, age: int):    # private setter
            if age > 15000 or age < 0:
                raise Exception('樹齡數字不合理。')
            self.__age = age
    
        def __del_age(self):              # private deleter(較少用)
            del self.__age
    
        # 以下就是property了,請「畫重點」。
        # fget, fset, fdel都是property()的關鍵字參數。顧名思義,不必解釋。
        breed = property(fget=__get_breed)   # breed和age是property。
        age = property(fget=__get_age, fset=__set_age, fdel=__del_age)
    
    主程式長這樣:
    try:
        tree = Tree('cedar', 150)      # 建立物件時設定初值:雪松, 150歲。
        print(f'{tree.breed=}')        # 貌似attribute,實則property。
        print(f'aa: {tree.age=}')    
        tree.age = -100                # 賦值(這裡會產生Exception)。
        print(f'bb: {tree.age=}')      # 這行沒有機會執行。
    except Exception as e:
        print(f'錯誤訊息:{str(e)}')
    finally:
        print(f'cc: {tree.age=}')
    
    輸出如下:
    https://ithelp.ithome.com.tw/upload/images/20220925/20148485jTag9Nr4kF.png
  • 以上tree.breed中的breed,以及tree.age中的age,名稱前無前綴底線,看來是公開屬性(public attributes),其實都是properties。
  • print(f'{tree.breed=}')tree.age = -100的寫法,和存取普通變數形式一致,相當直觀。
  • print(f'{tree.breed=}')中的tree.breed,骨子裡是呼叫類別內部的私有方法__get_breed()
  • 同樣道理,tree.age = -100其實是呼叫類別內部的私有方法__set_age()。執行這行時,-100超出設定的合理範圍,因而觸發Exception。所以print(f'bb: {tree.age=}')這行不會執行,直接跳到finally區塊。
  • 以上是Python的property入門。明天介紹property更「聰明進階」的用法。

註1: 假設不使用Day8「在外部修改私有屬性,真行嗎?」篇提到的方法。
註2: 語法糖(syntatic sugar)又稱語法甜頭(筆者個人較傾向此名)。是程式語言的一種機制,在和原本語法效果完全相同前提下,提供另一種比較簡短的語法。像C語言的i++就可以算是一種語法甜頭,它和i = i + 1等價。Python的i += 1也是,只是沒有i++那麼「甜」而已。


上一篇
Class為甚麼需要Private Attributes?
下一篇
Property Decorator
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言